﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
using CustomNetworking;
using BB;
using System.Timers;
using System.IO;
using System.Threading.Tasks;

namespace Boggle
{
    class BoggleServer
    {
        private TcpListener listener;

        // a queue of connections waiting to play
        private Queue<ClientConnection> connections;
        
        // the set of sockets that have not yet received a valid 'PLAY' command
        private HashSet<StringSocket> sockets;

        private string customBoard;

        public int activeGames { get { return currentGames.Count; } }

        private int gameTime;

        private static HashSet<string> dictionary;
        private HashSet<BoggleGame> currentGames;

        /// <summary>
        /// Instantiates a new BoggleServer.
        /// </summary>
        /// <param name="time">The time in seconds of games on this server</param>
        /// <param name="path">The path to the dictionary file</param>
        /// <param name="board">An optional string specifying the board layout.</param>
        public BoggleServer(int time, string path, string board = "")
        {
            gameTime = time;
            customBoard = board;
            dictionary = LoadDictionary(path);

            connections = new Queue<ClientConnection>();
            sockets = new HashSet<StringSocket>();
            currentGames = new HashSet<BoggleGame>();

            listener = new TcpListener(IPAddress.Any, 2000);
            listener.Start();
            listener.BeginAcceptSocket(ConnectionRequested, null);
        }

        /// <summary>
        /// Reads a list words from the specified file into a HashSet of strings.
        /// </summary>
        /// <param name="filename">The path of the dictionary file.</param>
        /// <returns>A set of all the words in the dictionary file.</returns>
        private HashSet<string> LoadDictionary(string filename)
        {
            HashSet<string> set = new HashSet<string>();

            using (StreamReader reader = new StreamReader(filename))
            {
                string line;
                while ((line = reader.ReadLine()) != null)
                {
                    set.Add(line);
                }
            }

            return set;
        }

        /// <summary>
        /// Stops the server's TcpListener.
        /// </summary>
        public void Stop()
        {
            listener.Stop();
        }

        /// <summary>
        /// Callback for new socket connection requests.
        /// </summary>
        public void ConnectionRequested(IAsyncResult result)
        {
            try
            {
                // get the socket and listen for a new connection request
                Socket s = listener.EndAcceptSocket(result);
                listener.BeginAcceptSocket(ConnectionRequested, null);

                // wrap the socket in a StringSocket and begin receiving
                StringSocket ss = new StringSocket(s, new UTF8Encoding());
                ss.BeginReceive(NewConnectionCallback, ss);
                sockets.Add(ss);
            }
            catch (Exception)
            {
                // we gotta do something here... not sure what
            }
        }

        /// <summary>
        /// Callback for connections that have been wrapped in a StringSocket.
        /// 
        /// Will ignore any commands besides "PLAY NAME".
        /// </summary>
        public void NewConnectionCallback(string s, Exception e, object payload)
        {
            StringSocket ss = (StringSocket)payload;

            try
            {
                {
                    string command = s.ToUpper();

                    if (command.Substring(0, 5).Equals("PLAY "))
                    {
                        lock (connections)
                        {
                            sockets.Remove(ss);

                            string name = s.Substring(5).Replace("\r", "");
                            connections.Enqueue(new ClientConnection(name, ss));
                            PairConnections();
                        }
                    }
                    else
                    {
                        ss.BeginSend("IGNORING " + s + "\n", (x, y) => { }, null);
                        ss.BeginReceive(NewConnectionCallback, ss);
                    }
                }
            }
            catch (Exception)
            {
                // the underlying socket has been closed, so remove it
                sockets.Remove(ss);
            }
        }

        /// <summary>
        /// Removes finished or terminated games from the set of active games.
        /// </summary>
        /// <param name="game">The finished game that is being removed.</param>
        public void EndGame(BoggleGame game)
        {
            lock (currentGames)
            {
                currentGames.Remove(game);
            }
        }

        /// <summary>
        /// Attempts to pair two client connections.
        /// </summary>
        public void PairConnections()
        {
            if (connections.Count > 1)
            {
                ClientConnection c1 = connections.Dequeue();
                ClientConnection c2 = connections.Dequeue();

                c1.opponent = c2;
                c2.opponent = c1;

                BoggleGame bg;

                if (customBoard.Length == 16)
                {
                    bg = new BoggleGame(c1, c2, gameTime, dictionary, this, customBoard);
                }
                else
                {
                    bg = new BoggleGame(c1, c2, gameTime, dictionary, this);
                }

                lock (currentGames)
                {
                    currentGames.Add(bg);
                }

                // create a new task and start the game on it
                Task t1 = new Task(() => bg.StartGame());
                t1.Start();
            }
        }
    }
}